/*
 * $Id: mxGraphSelectionModel.java,v 1.10 2011-01-06 08:14:09 gaudenz Exp $
 * Copyright (c) 2001-2005, Gaudenz Alder
 * 
 * All rights reserved.
 * 
 * See LICENSE file for license details. If you are unable to locate
 * this file please contact info (at) jgraph (dot) com.
 */
package com.mxgraph.view;

import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
import com.mxgraph.util.mxUndoableEdit;
import com.mxgraph.util.mxUndoableEdit.mxUndoableChange;

import java.util.*;

/**
 * Implements the selection model for a graph.
 * <p/>
 * This class fires the following events:
 * <p/>
 * mxEvent.UNDO fires after the selection was changed in changeSelection. The
 * <code>edit</code> property contains the mxUndoableEdit which contains the
 * mxSelectionChange.
 * <p/>
 * mxEvent.CHANGE fires after the selection changes by executing an
 * mxSelectionChange. The <code>added</code> and <code>removed</code>
 * properties contain Collections of cells that have been added to or removed
 * from the selection, respectively.
 * <p/>
 * To add a change listener to the graph selection model:
 * <p/>
 * <code>
 * addListener(
 * mxEvent.CHANGE, new mxIEventListener()
 * {
 * public void invoke(Object sender, mxEventObject evt)
 * {
 * mxGraphSelectionModel model = (mxSelectionModel) sender;
 * Collection added = (Collection) evt.getProperty("added");
 * Collection removed = (Collection) evt.getProperty("removed");
 * selectionChanged(model, added, removed);
 * }
 * });
 * </code>
 */
public class mxGraphSelectionModel extends mxEventSource {

    /**
     * Reference to the enclosing graph.
     */
    protected mxGraph graph;

    /**
     * Specifies if only one selected item at a time is allowed.
     * Default is false.
     */
    protected boolean singleSelection = false;

    /**
     * Holds the selection cells.
     */
    protected Set<Object> cells = new LinkedHashSet<Object>();

    /**
     * Constructs a new selection model for the specified graph.
     *
     * @param graph
     */
    public mxGraphSelectionModel(mxGraph graph) {
        this.graph = graph;
    }

    /**
     * @return the singleSelection
     */
    public boolean isSingleSelection() {
        return singleSelection;
    }

    /**
     * @param singleSelection the singleSelection to set
     */
    public void setSingleSelection(boolean singleSelection) {
        this.singleSelection = singleSelection;
    }

    /**
     * Returns true if the given cell is selected.
     *
     * @param cell
     * @return Returns true if the given cell is selected.
     */
    public boolean isSelected(Object cell) {
        return (cell == null) ? false : cells.contains(cell);
    }

    /**
     * Returns true if no cells are selected.
     */
    public boolean isEmpty() {
        return cells.isEmpty();
    }

    /**
     * Returns the number of selected cells.
     */
    public int size() {
        return cells.size();
    }

    /**
     * Clears the selection.
     */
    public void clear() {
        changeSelection(null, cells);
    }

    /**
     * Returns the first selected cell.
     */
    public Object getCell() {
        return (cells.isEmpty()) ? null : cells.iterator().next();
    }

    /**
     * Returns the selection cells.
     */
    public Object[] getCells() {
        return cells.toArray();
    }

    /**
     * Clears the selection and adds the given cell to the selection.
     */
    public void setCell(Object cell) {
        if (cell != null) {
            setCells(new Object[]{cell});
        } else {
            clear();
        }
    }

    /**
     * Clears the selection and adds the given cells.
     */
    public void setCells(Object[] cells) {
        if (cells != null) {
            if (singleSelection) {
                cells = new Object[]{getFirstSelectableCell(cells)};
            }

            List<Object> tmp = new ArrayList<Object>(cells.length);

            for (int i = 0; i < cells.length; i++) {
                if (graph.isCellSelectable(cells[i])) {
                    tmp.add(cells[i]);
                }
            }

            changeSelection(tmp, this.cells);
        } else {
            clear();
        }
    }

    /**
     * Returns the first selectable cell in the given array of cells.
     *
     * @param cells Array of cells to return the first selectable cell for.
     * @return Returns the first cell that may be selected.
     */
    protected Object getFirstSelectableCell(Object[] cells) {
        if (cells != null) {
            for (int i = 0; i < cells.length; i++) {
                if (graph.isCellSelectable(cells[i])) {
                    return cells[i];
                }
            }
        }

        return null;
    }

    /**
     * Adds the given cell to the selection.
     */
    public void addCell(Object cell) {
        if (cell != null) {
            addCells(new Object[]{cell});
        }
    }

    /**
     *
     */
    public void addCells(Object[] cells) {
        if (cells != null) {
            Collection<Object> remove = null;

            if (singleSelection) {
                remove = this.cells;
                cells = new Object[]{getFirstSelectableCell(cells)};
            }

            List<Object> tmp = new ArrayList<Object>(cells.length);

            for (int i = 0; i < cells.length; i++) {
                if (!isSelected(cells[i]) && graph.isCellSelectable(cells[i])) {
                    tmp.add(cells[i]);
                }
            }

            changeSelection(tmp, remove);
        }
    }

    /**
     * Removes the given cell from the selection.
     */
    public void removeCell(Object cell) {
        if (cell != null) {
            removeCells(new Object[]{cell});
        }
    }

    /**
     *
     */
    public void removeCells(Object[] cells) {
        if (cells != null) {
            List<Object> tmp = new ArrayList<Object>(cells.length);

            for (int i = 0; i < cells.length; i++) {
                if (isSelected(cells[i])) {
                    tmp.add(cells[i]);
                }
            }

            changeSelection(null, tmp);
        }
    }

    /**
     *
     */
    protected void changeSelection(Collection<Object> added,
                                   Collection<Object> removed) {
        if ((added != null && !added.isEmpty())
                || (removed != null && !removed.isEmpty())) {
            mxSelectionChange change = new mxSelectionChange(this, added,
                    removed);
            change.execute();
            mxUndoableEdit edit = new mxUndoableEdit(this, false);
            edit.add(change);
            fireEvent(new mxEventObject(mxEvent.UNDO, "edit", edit));
        }
    }

    /**
     *
     */
    protected void cellAdded(Object cell) {
        if (cell != null) {
            cells.add(cell);
        }
    }

    /**
     *
     */
    protected void cellRemoved(Object cell) {
        if (cell != null) {
            cells.remove(cell);
        }
    }

    /**
     *
     */
    public static class mxSelectionChange implements mxUndoableChange {

        /**
         *
         */
        protected mxGraphSelectionModel model;

        /**
         *
         */
        protected Collection<Object> added, removed;

        /**
         * @param model
         * @param added
         * @param removed
         */
        public mxSelectionChange(mxGraphSelectionModel model,
                                 Collection<Object> added, Collection<Object> removed) {
            this.model = model;
            this.added = (added != null) ? new ArrayList<Object>(added) : null;
            this.removed = (removed != null) ? new ArrayList<Object>(removed)
                    : null;
        }

        /**
         *
         */
        public void execute() {
            if (removed != null) {
                Iterator<Object> it = removed.iterator();

                while (it.hasNext()) {
                    model.cellRemoved(it.next());
                }
            }

            if (added != null) {
                Iterator<Object> it = added.iterator();

                while (it.hasNext()) {
                    model.cellAdded(it.next());
                }
            }

            Collection<Object> tmp = added;
            added = removed;
            removed = tmp;
            model.fireEvent(new mxEventObject(mxEvent.CHANGE, "added", added,
					"removed", removed));
		}

	}

}
